package com.luych.toolbox.data.service.service;

import com.luych.toolbox.common.enums.DataCompareHistoryColumnLevel;
import com.luych.toolbox.common.enums.DataCompareHistoryStatus;
import com.luych.toolbox.common.enums.DataCompareHistoryTableLevel;
import com.luych.toolbox.common.event.base.service.publisher.WebSocketEventPublisher;
import com.luych.toolbox.common.feign.intf.base.service.UserFeign;
import com.luych.toolbox.common.feign.param.data.service.DataCompareHistoryParam;
import com.luych.toolbox.common.feign.param.gateway.center.WebSocketEvent;
import com.luych.toolbox.common.feign.view.data.service.*;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import com.luych.toolbox.data.service.constant.Constant;
import com.luych.toolbox.data.service.dao.*;
import com.luych.toolbox.data.service.entity.*;
import com.luych.toolbox.data.service.job.DataCompareHistoryExecuteJob;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.Predicate;
import java.util.*;

@Service
public class DataCompareHistoryService
        extends BaseService<DataCompareHistoryView, DataCompareHistoryParam, DataCompareHistory>
        implements ApplicationListener<ApplicationReadyEvent> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataCompareHistoryService.class);

    private static final BaseConvert<DataCompareHistoryParam, DataCompareHistory> dataCompareHistoryParamToEntity = DataCompareHistory::new;
    private static final BaseConvert<DataCompareHistory, DataCompareHistoryView> dataCompareHistoryEntityToView = DataCompareHistoryView::new;
    private static final BaseConvert<DataCompareHistoryTable, DataCompareHistoryTableView> dataCompareHistoryTableEntityToView = DataCompareHistoryTableView::new;
    private static final BaseConvert<DataCompareHistoryColumn, DataCompareHistoryColumnView> dataCompareHistoryColumnEntityToView = DataCompareHistoryColumnView::new;
    private static final BaseConvert<DataComparePlan, DataComparePlanView> dataComparePlanEntityToView = DataComparePlanView::new;
    private static final BaseConvert<DataSource, DataSourceView> dataSourceEntityToView = DataSourceView::new;

    private final Boolean rescheduleWhenStart;

    private final Scheduler scheduler;
    private final TransactionTemplate transactionTemplate;

    private final WebSocketEventPublisher eventPublisher;

    private final DataCompareHistoryDao dataCompareHistoryDao;
    private final DataComparePlanDao dataComparePlanDao;
    private final DataCompareHistoryTableDao dataCompareHistoryTableDao;
    private final DataCompareHistoryColumnDao dataCompareHistoryColumnDao;
    private final DataSourceDao dataSourceDao;

    private final UserFeign userFeign;

    @Autowired
    public DataCompareHistoryService(@Value("${spring.quartz.rescheduleWhenStart:false}") Boolean rescheduleWhenStart,
                                     Scheduler scheduler,
                                     TransactionTemplate transactionTemplate,
                                     BusProperties busProperties,
                                     ApplicationEventPublisher applicationEventPublisher,
                                     DataCompareHistoryDao dataCompareHistoryDao,
                                     DataComparePlanDao dataComparePlanDao,
                                     DataCompareHistoryTableDao dataCompareHistoryTableDao,
                                     DataCompareHistoryColumnDao dataCompareHistoryColumnDao,
                                     DataSourceDao dataSourceDao,
                                     UserFeign userFeign) {
        super(dataCompareHistoryDao, dataCompareHistoryParamToEntity, dataCompareHistoryEntityToView);

        this.rescheduleWhenStart = rescheduleWhenStart;
        this.scheduler = scheduler;
        this.transactionTemplate = transactionTemplate;
        this.dataCompareHistoryDao = dataCompareHistoryDao;
        this.dataComparePlanDao = dataComparePlanDao;
        this.dataCompareHistoryTableDao = dataCompareHistoryTableDao;
        this.dataCompareHistoryColumnDao = dataCompareHistoryColumnDao;
        this.dataSourceDao = dataSourceDao;
        this.userFeign = userFeign;

        this.eventPublisher = new WebSocketEventPublisher(busProperties, applicationEventPublisher);
    }

    @Override
    protected DataCompareHistoryView fill(DataCompareHistoryView view) {
        Optional<DataCompareHistory> dataCompareHistoryOpt = dataCompareHistoryDao.findById(view.getId());
        dataCompareHistoryOpt.map(DataCompareHistory::getDataSourceOneId).flatMap(dataSourceDao::findById).map(dataSourceEntityToView).ifPresent(view::setDataSourceOne);
        dataCompareHistoryOpt.map(DataCompareHistory::getDataSourceTwoId).flatMap(dataSourceDao::findById).map(dataSourceEntityToView).ifPresent(view::setDataSourceTwo);
        dataCompareHistoryOpt.map(DataCompareHistory::getDataComparePlanId).flatMap(dataComparePlanDao::findById).map(dataComparePlanEntityToView).ifPresent(view::setDataComparePlan);
        dataCompareHistoryOpt.map(DataCompareHistory::getOwnerId).map(userFeign::get).ifPresent(view::setOwner);
        return super.fill(view);
    }

    private void setStatus(String id, DataCompareHistoryStatus status) {
        DataCompareHistory dataCompareHistory = dataCompareHistoryDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COMPARE_HISTORY_NOT_FOUND));
        dataCompareHistory.setStatus(status);
        dataCompareHistoryDao.save(dataCompareHistory);
    }

    private JobKey getJobKey(String id) {
        return new JobKey(id, DataCompareHistoryExecuteJob.class.getName());
    }

    private JobDetail getJobDetail(String id) {
        Map<String, Object> jobParam = new HashMap<String, Object>() {{
            this.put("id", id);
        }};
        return JobBuilder.newJob(DataCompareHistoryExecuteJob.class)
                .withIdentity(this.getJobKey(id))
                .usingJobData(new JobDataMap(jobParam))
                .build();
    }

    private Trigger getJobTrigger(String id) {
        return TriggerBuilder.newTrigger()
                .withIdentity(id, DataCompareHistoryExecuteJob.class.getName())
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(1, 1))
                .build();
    }

    @Override
    public DataCompareHistoryView create(DataCompareHistoryParam param) {
        Assert.notNull(param.getDataSourceOneId(), "Param dataSourceOneId can't be empty");
        Assert.notNull(param.getDataSourceTwoId(), "Param dataSourceTwoId can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getOwnerId(), "Param ownerId can't be empty");

        // save
        DataCompareHistory dataCompareHistoryParam = dataCompareHistoryParamToEntity.apply(param);
        dataCompareHistoryParam.setStatus(DataCompareHistoryStatus.LOADING);
        DataCompareHistory dataCompareHistoryResult = dataCompareHistoryDao.save(dataCompareHistoryParam);
        DataCompareHistoryView view = this.fill(dataCompareHistoryEntityToView.apply(dataCompareHistoryResult));

        // compare
        try {
            scheduler.scheduleJob(this.getJobDetail(view.getId()), this.getJobTrigger(view.getId()));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        return view;
    }

    @Override
    public DataCompareHistoryView modify(DataCompareHistoryParam param) {
        throw new HttpException(HttpStatus.METHOD_NOT_ALLOWED, Constant.DATA_COMPARE_HISTORY_MODIFY_REJECT);
    }

    @Override
    public void remove(String id) {
        DataCompareHistory dataCompareHistory = dataCompareHistoryDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COMPARE_HISTORY_NOT_FOUND));
        try {
            scheduler.deleteJob(this.getJobKey(id));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
        DataCompareHistoryStatus oldStatus = dataCompareHistory.getStatus();

        dataCompareHistory.setStatus(DataCompareHistoryStatus.REMOVING);
        dataCompareHistoryDao.save(dataCompareHistory);

        eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.DATA_COMPARE_REMOVING, id));

        try {
            LOGGER.info("start to delete compare result of id [{}]...", id);
            transactionTemplate.executeWithoutResult(status -> {
                dataCompareHistoryTableDao.deleteByDataCompareHistoryId(id);
                dataCompareHistoryColumnDao.deleteByDataCompareHistoryId(id);
                dataCompareHistoryDao.deleteById(id);
            });
            LOGGER.info("success to delete compare result of id [{}]...", id);
        } finally {
            if (dataCompareHistoryDao.existsById(id)) {
                dataCompareHistory.setStatus(oldStatus);
                dataCompareHistoryDao.save(dataCompareHistory);
            }
        }
    }

    public PageResult<DataCompareHistoryView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<DataCompareHistory> page;
        if (StringUtils.isEmpty(keyword)) {
            page = dataCompareHistoryDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = dataCompareHistoryDao.findByNameLike("%" + keyword + "%", new SpringPageParamAdapter(pageParam));
        }
        PageResult<DataCompareHistory> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataCompareHistoryEntityToView).convert(this::fill);
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        if (Boolean.TRUE.equals(rescheduleWhenStart)) {
            List<DataCompareHistoryStatus> statuses = Arrays.asList(DataCompareHistoryStatus.LOADING, DataCompareHistoryStatus.RUNNING);
            List<DataCompareHistory> dataCompareHistorys = dataCompareHistoryDao.findByStatusIn(statuses);
            for (DataCompareHistory dataCompareHistory : dataCompareHistorys) {
                try {
                    scheduler.deleteJob(this.getJobKey(dataCompareHistory.getId()));
                    scheduler.scheduleJob(this.getJobDetail(dataCompareHistory.getId()), this.getJobTrigger(dataCompareHistory.getId()));
                    this.setStatus(dataCompareHistory.getId(), DataCompareHistoryStatus.RUNNING);
                } catch (SchedulerException e) {
                    this.setStatus(dataCompareHistory.getId(), DataCompareHistoryStatus.EXCEPTION);
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public PageResult<DataCompareHistoryTableView> pageCompareTables(PageParam pageParam, String id, Boolean diff, String table) {
        Specification<DataCompareHistoryTable> specification = (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            Predicate historyEqual = criteriaBuilder.equal(root.get("dataCompareHistoryId"), id);
            predicates.add(historyEqual);
            if (!StringUtils.isEmpty(table)) {
                Predicate tableOneLike = criteriaBuilder.like(root.get("tableOne"), '%' + table + '%');
                Predicate tableTwoLike = criteriaBuilder.like(root.get("tableTwo"), '%' + table + '%');
                Predicate tableLike = criteriaBuilder.or(tableOneLike, tableTwoLike);
                predicates.add(tableLike);
            }
            if (Boolean.TRUE.equals(diff)) {
                Predicate diffPredicate = criteriaBuilder.notEqual(root.get("level"), DataCompareHistoryTableLevel.SAME);
                predicates.add(diffPredicate);
            }
            if (Boolean.FALSE.equals(diff)) {
                Predicate samePredicate = criteriaBuilder.equal(root.get("level"), DataCompareHistoryTableLevel.SAME);
                predicates.add(samePredicate);
            }
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
        Page<DataCompareHistoryTable> page = dataCompareHistoryTableDao.findAll(specification, new SpringPageParamAdapter(pageParam));

        PageResult<DataCompareHistoryTable> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataCompareHistoryTableEntityToView);
    }

    public PageResult<DataCompareHistoryColumnView> pageCompareColumns(PageParam pageParam, String id, Boolean diff, String table, String column) {
        Specification<DataCompareHistoryColumn> specification = (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            Predicate historyEqual = criteriaBuilder.equal(root.get("dataCompareHistoryId"), id);
            predicates.add(historyEqual);
            if (!StringUtils.isEmpty(table)) {
                Predicate tableOneEqual = criteriaBuilder.equal(root.get("tableOne"), table);
                Predicate tableTwoEqual = criteriaBuilder.equal(root.get("tableTwo"), table);
                Predicate tableEqual = criteriaBuilder.or(tableOneEqual, tableTwoEqual);
                predicates.add(tableEqual);
            }
            if (!StringUtils.isEmpty(column)) {
                Predicate columnOneLike = criteriaBuilder.like(root.get("columnOne"), '%' + column + '%');
                Predicate columnTwoLike = criteriaBuilder.like(root.get("columnTwo"), '%' + column + '%');
                Predicate columnLike = criteriaBuilder.or(columnOneLike, columnTwoLike);
                predicates.add(columnLike);
            }
            if (Boolean.TRUE.equals(diff)) {
                Predicate diffPredicate = criteriaBuilder.notEqual(root.get("level"), DataCompareHistoryColumnLevel.SAME);
                predicates.add(diffPredicate);
            }
            if (Boolean.FALSE.equals(diff)) {
                Predicate samePredicate = criteriaBuilder.equal(root.get("level"), DataCompareHistoryColumnLevel.SAME);
                predicates.add(samePredicate);
            }
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
        Page<DataCompareHistoryColumn> page = dataCompareHistoryColumnDao.findAll(specification, new SpringPageParamAdapter(pageParam));

        PageResult<DataCompareHistoryColumn> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataCompareHistoryColumnEntityToView);
    }

    @Transactional
    public void truncate() {
        dataCompareHistoryColumnDao.truncate();
        dataCompareHistoryTableDao.truncate();
        dataCompareHistoryDao.truncate();
    }
}
